//    jen-breakout is the game of breakout implemented in C, using SDL.
//    Copyright 2009 Nathan Michaels
//
//    This file is part of jen-breakout.
//
//    jen-breakout is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 2 of the License, or
//    (at your option) any later version.
//
//    jen-breakout is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with jen-breakout.  If not, see <http://www.gnu.org/licenses/>.

#include <stdlib.h>
#include <memory.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <getopt.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <SDL/SDL_ttf.h>
#include "SDL_collide.h"
#include "sdl-types.h"
#include "levels.h"
#include "highscores.h"
#include "breakout.h"

#define SCREEN_BPP    32
#define FONT_SIZE     18

#define MAX_FPS 60
#define MS_PER_FRAME ((int)((1000.0 / MAX_FPS) + 0.5))
#define BALL_SPEED 1
#define NUM_BRICKS 6 // This should probably be a sizeof.
#define PADDLE_SPEED 2
#define SPEED_FACTOR 2
#define START_LIVES 3
#define SKIP 1 // For SDL_collide's pixel-perfect detection.

#define DEFAULT_FLAGS SDL_FULLSCREEN

typedef enum
{
    NONE = 0,
    SIDE = 1,
    END  = 2, // top or bottom
} collision_side_t;

typedef enum
{
    QUIT,
    PRESTART,
    PREPRESTART,
    PAUSE,
    RUN,
    WIN,
    LOSE
} game_mode_t;

typedef struct
{
    double ballSpeed;
    int paddleSpeed;
    int speedFactor;
    int baseSpeed;
    int livesLeft;
    int firstRow;
    int lastRow;
    int levelNum;
    level_ptr_t level;
    game_mode_t mode;
    boolean_t   demoMode;
    boolean_t   fastMode;
    boolean_t   angles;
} game_state_t;

typedef struct
{
    int vx;
    int vy;
    int x;
    int y;
    SDL_SurfacePtr surfacePtr;
} object_t, *object_ptr_t;

typedef struct
{
    double vx;
    double vy;
    double x;
    double y;
    SDL_SurfacePtr surfacePtr;
} mobile_object_t, *mobile_object_ptr_t;

static SDL_Rect gameRect =
{
    .x = (SCREEN_WIDTH - GAME_WIDTH) / 2,
    .y = (SCREEN_HEIGHT - (GAME_HEIGHT + DASH_HEIGHT)) / 2,
    .w = GAME_WIDTH,
    .h = GAME_HEIGHT
};

static SDL_Rect dashRect =
{
    .x = (SCREEN_WIDTH - GAME_WIDTH) / 2,
    .y = GAME_HEIGHT + ((SCREEN_HEIGHT - (GAME_HEIGHT + DASH_HEIGHT)) / 2),
    .w = GAME_WIDTH,
    .h = DASH_HEIGHT
};

static game_state_t gameState =
{
    .mode = PRESTART,
    .demoMode    = FALSE,
    .fastMode    = FALSE,
    .ballSpeed   = BALL_SPEED,
    .paddleSpeed = PADDLE_SPEED,
    .speedFactor = SPEED_FACTOR,
    .baseSpeed   = SPEED_FACTOR,
    .levelNum    = 0,
    .livesLeft   = START_LIVES,
    .angles      = TRUE,
    .firstRow = 0,
    .lastRow  = 0,
    .level    = NULL
};
    
static char *brickNames[] =
{
    "redbrick.png",
    "orangebrick.png",
    "yellowbrick.png",
    "greenbrick.png",
    "bluebrick.png",
    "purplebrick.png"
};

// If there's more than one kind of brick, they had better all have
// the same dimensions.
static SDL_SurfacePtr brickPtrs[NUM_BRICKS] = {NULL};
static SDL_SurfacePtr screenPtr = NULL;
static SDL_SurfacePtr paddlePtr = NULL;
static SDL_SurfacePtr ballPtr   = NULL;
static SDL_SurfacePtr scorePtr  = NULL;
static SDL_SurfacePtr iconPtr   = NULL;

static TTF_Font *fontPtr = NULL;
static SDL_Color textColor = {0x00, 0x00, 0x00};
static SDL_Color bgColor = {0xFF, 0xFF, 0xCC};
static SDL_Color dashColor = {0xFF, 0xFF, 0xFF};
static uint32_t  fullScreen = DEFAULT_FLAGS;

static int bricksLeft;
static object_t bricks[BRICK_ROWS][BRICK_COLS];
static mobile_object_t paddle;
static mobile_object_t ball;

static uint32_t score;

// These are for timing things.
boolean_t profiling;
uint32_t maxWait;
uint32_t minWait;

static void CleanUp (void)
{
    int brick;

    if (iconPtr != NULL)
        SDL_FreeSurface (iconPtr);

    if (scorePtr != NULL)
        SDL_FreeSurface (scorePtr);

    for (brick = 0; brick < NUM_BRICKS; brick++)
        if (brickPtrs[brick] != NULL)
            SDL_FreeSurface (brickPtrs[brick]);
    if (screenPtr != NULL)
        SDL_FreeSurface (screenPtr);
    if (paddlePtr != NULL)
        SDL_FreeSurface (paddlePtr);
    if (ballPtr != NULL)
        SDL_FreeSurface (ballPtr);

    if (fontPtr != NULL)
        TTF_CloseFont (fontPtr);

    TTF_Quit ();
    SDL_Quit ();
}

// Message is an optional message to be printed on failure.
// This is really just a more polite way to spell assert.
static void QuitIf (boolean_t condition, char *message)
{
    char *errorMsg = NULL;
    err_fun errFuns[] = {TTF_GetError, SDL_GetError, IMG_GetError};
    int fcnIdx;
    
    if (condition)
    {
        if (message != NULL)
            fprintf (stderr, message);
        
        for (fcnIdx = 0; fcnIdx < sizeof (errFuns); fcnIdx++)
        {
            errorMsg = errFuns[fcnIdx] ();
            
            if (errorMsg != NULL)
                break;
        }
        if (errorMsg != NULL)
            fprintf (stderr, "\t%s\n", errorMsg);

        CleanUp ();
        exit (1);
    }
}


static SDL_SurfacePtr LoadImage (char *fileName)
{
    SDL_SurfacePtr loadedImg = NULL;
    SDL_SurfacePtr optimizedImg = NULL;

    loadedImg = IMG_Load (fileName);
    QuitIf (loadedImg == NULL, "Failed to load image.\n");
    optimizedImg = SDL_DisplayFormatAlpha (loadedImg);
    SDL_FreeSurface (loadedImg);

    return optimizedImg;
}


static void InitSDL (void)
{
    uint32_t flags;

    // Other flags: SDL_INIT_AUDIO, SDL_INIT_CDROM, SDL_INIT_JOYSTICK
    flags = SDL_INIT_TIMER | SDL_INIT_VIDEO;
    QuitIf (SDL_Init (flags) != 0, "Unable to init SDL.\n");
    QuitIf (TTF_Init () != 0, "Failed to init TTF.\n");

    // Set up an icon!
    iconPtr = SDL_LoadBMP ("icon16.bmp");
    QuitIf (iconPtr == NULL, "Couldn't load icon!");
    SDL_WM_SetIcon(iconPtr, NULL);

    flags = fullScreen | SDL_HWSURFACE | SDL_DOUBLEBUF;
    if (SDL_VideoModeOK (SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, flags))
        screenPtr = SDL_SetVideoMode (SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,
                                      flags);
    else
    {
        flags = fullScreen | SDL_SWSURFACE;
        if (SDL_VideoModeOK (SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, flags))
        {
            printf ("Not using hardware acceleration.\n");
            screenPtr = SDL_SetVideoMode (SCREEN_WIDTH, SCREEN_HEIGHT,
                                          SCREEN_BPP, flags);
        }
        else
            QuitIf (TRUE, "Couldn't get an acceptable video mode.\n");
    }
                
    QuitIf (screenPtr == NULL, "Failed to init screen.\n");

    SDL_EnableKeyRepeat (0, 0); // Disable key repeating.

    SDL_WM_SetCaption ("Breakout!", NULL);
    SDL_ShowCursor (SDL_DISABLE);
}


static void LoadImages (void)
{
    int brick;
    
    paddlePtr = LoadImage ("paddle.png");
    ballPtr   = LoadImage ("ball.png");

    for (brick = 0; brick < NUM_BRICKS; brick++)
    {
        brickPtrs[brick]  = LoadImage (brickNames[brick]);
        QuitIf (brickPtrs[brick] == NULL, "Failed to load brick.\n");
    }

    fontPtr   = TTF_OpenFont ("font.ttf", FONT_SIZE);
    QuitIf (paddlePtr == NULL || ballPtr == NULL || fontPtr == NULL,
            "Failed to load images.\n");
}


static void ApplySurface (int x, int y,
                          SDL_SurfacePtr source,
                          SDL_SurfacePtr dest)
{
    SDL_Rect offset;

    offset.x = x;
    offset.y = y;

    SDL_BlitSurface (source, NULL, dest, &offset);
}


static void ResetBricks (SDL_SurfacePtr brickArray[], level_ptr_t levelPtr,
                         boolean_t init)
{
    int row, col, brickIdx;
    object_ptr_t thisBrick;

    bricksLeft = 0;
    gameState.firstRow = levelPtr->firstRow;
    gameState.lastRow  = levelPtr->lastRow;

    for (row = 0; row < BRICK_ROWS; row++)
        for (col = 0; col < BRICK_COLS; col++)
        {
            thisBrick = &(bricks[row][col]);

            brickIdx = levelPtr->level[row][col];
            if ((brickIdx > 0) && (brickIdx <= NUM_BRICKS))
            {
                brickIdx--;
                bricksLeft++;
                thisBrick->surfacePtr = brickArray[brickIdx];
            }
            else
                thisBrick->surfacePtr = NULL;

            if (init)
            {
                thisBrick->vx = 0;
                thisBrick->vy = 0;
                thisBrick->x  = col * brickArray[0]->w + gameRect.x;
                thisBrick->y  = row * brickArray[0]->h + gameRect.y;
            }
        }
}


static void ResetPaddle (SDL_SurfacePtr imgPtr)
{
    paddle.surfacePtr = imgPtr;
    paddle.vx = 0;
    paddle.vy = 0;
    // Start out in the middle, on the bottom..
    paddle.x = (GAME_WIDTH / 2) - (imgPtr->w / 2);
    paddle.y = GAME_HEIGHT - imgPtr->h;
}


static void InitPaddle (SDL_SurfacePtr imgPtr)
{
    paddle.surfacePtr = imgPtr;
    paddle.vx = 0;
    paddle.vy = 0;
    // Start out in the middle, on the bottom..
    paddle.x = (GAME_WIDTH / 2) - (imgPtr->w / 2);
    paddle.y = GAME_HEIGHT - imgPtr->h;
}


static void InitScore (void)
{
    char scoreStr[10];

    score = 0;
    sprintf (scoreStr, "%.6u", score);
    scorePtr = TTF_RenderText_Solid (fontPtr, scoreStr, textColor);
}


// Todo: turn RenderMessage and RenderName into 1 non-sucky function.
void RenderMessage (char *msg)
{
    SDL_SurfacePtr textPtr;
    SDL_Rect offset;

    textPtr = TTF_RenderText_Solid (fontPtr, msg, textColor);
    offset.x = 0;
    offset.y = (GAME_HEIGHT / 2) - (textPtr->h / 2);
    offset.w = GAME_WIDTH;
    offset.h = textPtr->h;
    SDL_FillRect (screenPtr, &offset, SDL_MapRGB (screenPtr->format,
                                                  bgColor.r, bgColor.g,
                                                  bgColor.b));
    offset.x = (GAME_WIDTH / 2) - (textPtr->w / 2);
    ApplySurface (offset.x, offset.y, textPtr, screenPtr);
    SDL_FreeSurface (textPtr);
    SDL_Flip (screenPtr);
}


void RenderName (char *name)
{
    SDL_SurfacePtr textPtr;
    SDL_Rect offset;

    textPtr = TTF_RenderText_Solid (fontPtr, name, textColor);
    offset.x = 0;
    offset.y = (GAME_HEIGHT / 2) + (textPtr->h / 2);
    offset.w = GAME_WIDTH;
    offset.h = textPtr->h;
    SDL_FillRect (screenPtr, &offset, SDL_MapRGB (screenPtr->format,
                                                  bgColor.r, bgColor.g,
                                                  bgColor.b));
    offset.x = (GAME_WIDTH / 2) - (textPtr->w / 2);
    SDL_BlitSurface (textPtr, NULL, screenPtr, &offset);
    SDL_FreeSurface (textPtr);
    SDL_Flip (screenPtr);
}

    
static void UpdateScore (void)
{
    static int lastScore = 0;
    char scoreStr[10];

    if (lastScore == score)
        return;

    lastScore = score;
    SDL_FreeSurface (scorePtr);
    sprintf (scoreStr, "%.6u", score);
    scorePtr = TTF_RenderText_Solid (fontPtr, scoreStr, textColor);
}


// Paddle must be reset before ball.
static void ResetBall (SDL_SurfacePtr imgPtr)
{
    ball.surfacePtr = imgPtr;
    ball.vx = gameState.ballSpeed;
    ball.vy = -1 * gameState.ballSpeed;
    // Start out on top of the paddle.
    ball.x = (SCREEN_WIDTH / 2) - (imgPtr->w / 2);
    ball.y = paddle.y - imgPtr->h;
}


// Paddle must be initialized before ball.
static void InitBall (SDL_SurfacePtr imgPtr)
{
    ball.surfacePtr = imgPtr;
    ball.vx = 0;
    ball.vy = 0;
    // Start out on top of the paddle.
    ball.x = (SCREEN_WIDTH / 2) - (imgPtr->w / 2);
    ball.y = paddle.y - imgPtr->h;
}


static void ClearScreen (void)
{
    SDL_FillRect (screenPtr, &gameRect,
                  SDL_MapRGB (screenPtr->format,
                              bgColor.r, bgColor.g, bgColor.b));
    SDL_FillRect (screenPtr, &dashRect,
                  SDL_MapRGB (screenPtr->format,
                              dashColor.r, dashColor.g, dashColor.b));
}


static void MoveBall (void)
{
    int leftWall, rightWall, ceiling, floor;
    

    leftWall = gameRect.x;
    rightWall = gameRect.x + gameRect.w - 1;
    ceiling = gameRect.y;
    floor = gameRect.y + gameRect.h - 1;

    ball.x += ball.vx;
    ball.y += ball.vy;

    if (gameState.mode == PRESTART)
    {
        ball.x = paddle.x + (paddle.surfacePtr->w / 2) - (ballPtr->w / 2);
        ball.y = paddle.y - ballPtr->h;
        return;
    }

    // Check for wall collisions while we're here and don't let the
    // ball go through walls.
    if (ball.x < leftWall)
    {
        ball.x = leftWall;
        ball.vx = -1 * ball.vx;
    }
    if (ball.y < ceiling)
    {
        ball.y = ceiling;
        ball.vy = -1 * ball.vy;
    }
    if (ball.x + ball.surfacePtr->w > rightWall)
    {
        ball.x = rightWall - ball.surfacePtr->w;
        ball.vx = -1 * ball.vx;
    }
    // Crashed into the floor...loser!
    if (ball.y + ball.surfacePtr->h > floor)
    {
        ball.y = floor - ball.surfacePtr->h;
        ball.vy = 0;
        ball.vx = 0;
        gameState.mode = LOSE;
    }
}


static void MovePaddle (void)
{
    // The paddle does not move up or down.
    if (gameState.demoMode)
        paddle.x = (ball.x + (ball.surfacePtr->w / 2)) -
            (paddle.surfacePtr->w / 2);
    else
        paddle.x += paddle.vx;
    if (paddle.x < gameRect.x)
        paddle.x = gameRect.x;
    if (paddle.x + paddle.surfacePtr->w > gameRect.w + gameRect.x - 1)
        paddle.x = gameRect.w + gameRect.x - 1 - paddle.surfacePtr->w;
}


// Assume A and B collide. Look at A's past to determine how they collided.
// Return a bit vector containing END if it was top/bottom and SIDE if it was
// on the side. If it's a corner, both bits will be set. The vx and vy
// parameters are for A.
static uint32_t CollisionType (SDL_SurfacePtr aSurf, SDL_Rect A, 
                               SDL_SurfacePtr bSurf, SDL_Rect B, int vx, int vy)
{
    uint32_t collisions;
    SDL_Rect Ax, Ay;

    // This collision could have happened in one of three ways:
    //  - A hits B from the side.
    //  - A hits B from the top or bottom ("end").
    //  - A hits B on a corner.
    // In all three ways, the following two statements must be true.
    //  - Part of A will be to the left of B's right and to the right of B's
    //    left.
    //  - Part of A will be below the top and above the bottom of B.
    // So in order to detect which side hit, we need to know more about
    // velocities. We can safely assume that in the last frame, the two objects
    // were not colliding. So, here's something we know about the last frame
    // for each case:
    //  - A hits B from the side.
    //    - No part of A was between the left and right sides of B.
    //  - A hits B from the top or bottom ("end").
    //    - No part of A was between the top and bottom of end.
    //  - A hits B on a corner.
    //    - If you extended all the lines that make up B, A would be completely
    //      inside the corners:
    //
    //                    |             |    -----
    //                    |             |    | A |
    //                    |             |    -----
    //         --------------------------------------
    //                    |      B      |
    //         --------------------------------------
    //                    |             |
    //                    |             |
    // More data:
    // If A would have still collided if it had only moved in the X axis, its
    // side hit. The same goes for Y and top/bottom. If both would have caused
    // collisions, we say the corner hit.

    collisions = 0;
    Ax = Ay = A;
    Ax.x -= vx;
    Ay.y -= vy;

    if (SDL_CollidePixel (aSurf, Ay.x, Ay.y,
                          bSurf, B.x, B.y, SKIP))
        collisions |= SIDE;
    if (SDL_CollidePixel (aSurf, Ax.x, Ax.y,
                          bSurf, B.x, B.y, SKIP))
        collisions |= END;
    return collisions;
}

static void DetectCollisions (void)
{
    collision_side_t side;
    int ballX, ballY;
    int row, col;
    int startRow, startCol;
    int endRow, endCol;
    SDL_Rect paddleOffset, ballOffset, brickOffset;

    if (ball.y + ball.surfacePtr->h >= paddle.y)
    {
        paddleOffset.x = paddle.x;
        paddleOffset.y = paddle.y;

        // First, the paddle.
        side = NONE;
        ballOffset.x = ball.x;
        ballOffset.y = ball.y;
        if (SDL_CollidePixel (ballPtr, ball.x, ball.y,
                              paddlePtr, paddle.x, paddle.y,
                              SKIP))
            side |= CollisionType (ballPtr, ballOffset, paddlePtr, 
                                   paddleOffset, ball.vx, ball.vy);

        if ((side & END) != 0)
        {
            // The paddle can only reflect balls upward. If it manages to grab
            // the ball, it should just keep sending it up.
            ball.vy = -1 * gameState.ballSpeed;

            if (gameState.angles)
            {
                // New feature: angled reflection!
                // First, find out if the ball is in the leftmost third.
                if (ball.x + ballPtr->w <= paddle.x + (paddlePtr->w / 3.0))
                    ball.vx -= .5;
                // Next, find out if it's in the rightmost third.
                else if (ball.x >=
                         paddle.x + paddlePtr->w - (paddlePtr->w / 3.0))
                    ball.vx += .5;
                // If neither, do nothing.
            }
        }
        if ((side & SIDE) != 0)
        {
            // Bug fix: if you hit the corner of the paddle with the ball or
            // hit the ball with the side of a moving paddle, it can go in too
            // deep to escape and the ball will just get stuck
            // bouncing around inside the paddle. Perhaps the proper
            // way to fix this is to have the paddle only move 1 pixel at a
            // time...but that's more work.
//            ball.y = paddle.y - ballPtr->h;
            ball.vx = -1 * ball.vx;
        }
            
        // Cheap optimization: if we hit the paddle, we won't hit any bricks.
        if (side != NONE)
            return;
    }

    // Pick a corner of the ball.
    ballX = ball.vx < 0 ? ball.x : ball.x + ball.surfacePtr->w;
    ballY = ball.vy < 0 ? ball.y : ball.y + ball.surfacePtr->h;
    // Find the row and column of bricks the corner's in.
    row = (ballY - gameRect.y) / (brickPtrs[0]->h);
    col = (ballX - gameRect.x) / (brickPtrs[0]->w);

    if (row > gameState.lastRow || row < gameState.firstRow)
        return;

    // Figure out which bricks to check for collisions (up to 4 of them)
    if (ball.vy < 0)
    {
        startRow = row - 1;
        endRow = row;
    }
    else
    {
        startRow = row;
        endRow = row + 1;
    }
    if (ball.vx > 0)
    {
        startCol = col;
        endCol = col + 1;
    }
    else
    {
        startCol = col - 1;
        endCol = col;
    }

    // Don't look for rows or columns that don't exist.
    startRow = startRow >= 0 ? startRow : 0;
    startCol = startCol >= 0 ? startCol : 0;
    endRow = endRow < BRICK_ROWS ? endRow : BRICK_ROWS - 1;
    endCol = endCol < BRICK_COLS ? endCol : BRICK_COLS - 1;

    for (row = startRow; row <= endRow; row++)
        for (col = startCol; col <= endCol; col++)
        {
            // Check for brick presence first.
            if (bricks[row][col].surfacePtr == NULL)
                continue;
            
            brickOffset.x = bricks[row][col].x;
            brickOffset.y = bricks[row][col].y;
            brickOffset.w = bricks[row][col].surfacePtr->w;
            brickOffset.h = bricks[row][col].surfacePtr->h;

            side = NONE;
            ballOffset.x = ball.x;
            ballOffset.y = ball.y;
            if (SDL_CollidePixel (ballPtr, ballOffset.x, ballOffset.y,
                                  brickPtrs[0], brickOffset.x, brickOffset.y,
                                  SKIP))
                side |= CollisionType (ballPtr, ballOffset, brickPtrs[0],
                                       brickOffset, ball.vx, ball.vy);
            if ((side & END) != 0)
                ball.vy = -1 * ball.vy;
            if ((side & SIDE) != 0)
                ball.vx = -1 * ball.vx;
            if (side != NONE)
            {
                // The brick got hit!
                bricks[row][col].surfacePtr = NULL;
                bricksLeft--;
                score += gameState.speedFactor;
            }
        }
}


static void RenderBricks (void)
{
    int row, col;
    object_ptr_t brick;

    for (row = 0; row < BRICK_ROWS; row++)
        for (col = 0; col < BRICK_COLS; col++)
        {
            brick = &(bricks[row][col]);
            if (brick->surfacePtr != NULL)
                ApplySurface (brick->x, brick->y, brick->surfacePtr,
                              screenPtr);
        }
}


static void RenderPaddle (void)
{
    ApplySurface ((int)paddle.x, (int)paddle.y, paddle.surfacePtr, screenPtr);
}


static void RenderBall (void)
{
    ApplySurface ((int)ball.x, (int)ball.y, ball.surfacePtr, screenPtr);
}


static void RenderScore (void)
{
    int life;
    int x, y;

    ApplySurface (dashRect.x, dashRect.y, scorePtr, screenPtr);
    for (life = gameState.livesLeft; life > 0; life--)
    {
        x = dashRect.x + dashRect.w - ((ballPtr->w + 1) * life);
        y = dashRect.y;
        ApplySurface (x, y, ballPtr, screenPtr);
    }
}


static void RenderHighScores (void)
{
    int idx, x, y;
    SDL_SurfacePtr scorePtr;
    char score[10 + NAME_LEN + 4]; // score len is 10.

    y = GAME_HEIGHT / (NUM_HIGH_SCORES);
    for (idx = 0; idx < NUM_HIGH_SCORES; idx++)
    {
        sprintf (score, "%s - %u", highScores[idx].name, highScores[idx].score);
        scorePtr = TTF_RenderText_Solid (fontPtr, score, textColor);
        x = (GAME_WIDTH / 2) - (scorePtr->w / 2);
        ApplySurface (x, y, scorePtr, screenPtr);
        y += scorePtr->h;
        SDL_FreeSurface (scorePtr);
    }
}


static void RenderEverything (void)
{
    ClearScreen  ();
    RenderBricks ();
    RenderPaddle ();
    RenderBall   ();
    RenderScore  ();
}


static void StartGame (void)
{
    gameState.mode = RUN;
    ball.vx = gameState.ballSpeed;
    ball.vy = -1 * gameState.ballSpeed;
}


static void HandleKeypress (SDL_Event *evPtr)
{
    boolean_t down;

    if (evPtr->type == SDL_KEYDOWN)
        down = TRUE;
    else if (evPtr->type == SDL_KEYUP)
        down = FALSE;
    else
    {
        down = FALSE; // Make the compiler happy.
        QuitIf (TRUE, "Invalid key event.\n");
    }

    switch (evPtr->key.keysym.sym)
    {
    case SDLK_LEFT:
        paddle.vx = down ? -1 * gameState.paddleSpeed : 0;
        break;
    case SDLK_RIGHT:
        paddle.vx = down ? gameState.paddleSpeed : 0;
        break;
    case SDLK_DOWN:
        gameState.speedFactor = gameState.baseSpeed;
        break;
    case SDLK_UP:
        gameState.speedFactor++;
        break;
    default:
        break;
    }

    // Control keys only care about the down event.
    if (!down)
        return;
    switch (evPtr->key.keysym.sym)
    {
    case SDLK_q:
    case SDLK_ESCAPE:
        gameState.mode = QUIT;
        break;
    case SDLK_SPACE:
        if (gameState.mode == PREPRESTART)
            gameState.mode = PRESTART;
        else if (gameState.mode == PRESTART)
            StartGame ();
        else if (gameState.mode == PAUSE)
            gameState.mode = RUN;
        else if (gameState.mode == RUN)
            gameState.mode = PAUSE;
        break;
    default:
        break;
    }
}


// Do not call this unless you actually have a high score to add.
static void  AddHighScore (uint32_t score)
{
    int idx, mover;

    for (idx = 0; idx < NUM_HIGH_SCORES; idx++)
        if (highScores[idx].score < score)
            break;
    
    for (mover = NUM_HIGH_SCORES - 2; mover >= idx; mover--)
        memcpy (&highScores[mover + 1], &highScores[mover],
                sizeof (high_score_t));

    memset (&highScores[idx], 0, sizeof (high_score_t));
    GetName (highScores[idx].name);
    highScores[idx].score = score;
    WriteHighScores ();
}


static void LoseGame (void)
{
    if (score > highScores[NUM_HIGH_SCORES - 1].score)
        AddHighScore (score);

    ResetLevels ();
    gameState.level = NextLevel ();
    gameState.levelNum = 0;
    ResetBricks (brickPtrs, gameState.level, FALSE);
    ResetBall (ballPtr);
    ResetPaddle (paddlePtr);
    score = 0;
    gameState.speedFactor = SPEED_FACTOR;
    gameState.baseSpeed = SPEED_FACTOR;
    gameState.mode = PREPRESTART;
    gameState.livesLeft = START_LIVES;
    RenderHighScores ();
    //RenderMessage ("Fortune cookie say: You lose, but you tried "
    //"really hard.");
}


void MainLoop (void)
{
    SDL_Event event;
    int startTime, now;
    int iter;
    uint32_t waitTime;

    maxWait = 0;
    minWait = 0xFFFFFFFF;
    if (gameState.demoMode)
        StartGame ();
    else
        gameState.mode = PRESTART;

    while (gameState.mode != QUIT)
    {
        startTime = SDL_GetTicks ();
        // Poll for events.
        while (SDL_PollEvent (&event))
            switch (event.type)
            {
                // Handle quitting in here.
            case SDL_QUIT:
                gameState.mode = QUIT;
                break;
            case SDL_KEYDOWN:
            case SDL_KEYUP:
                HandleKeypress (&event);
                break;
            }

        // Game state gets changed during keyboard events, so check that here.
        if (gameState.mode != RUN && gameState.mode != PRESTART)
            continue;

        for (iter = 0; iter < gameState.speedFactor; iter++)
        {
            // Move the ball and paddle.
            MovePaddle ();
            MoveBall ();
            
            DetectCollisions ();

            UpdateScore ();
            if (bricksLeft <= 0)
            {
                ball.vx = 0;
                ball.vy = 0;
                gameState.mode = WIN;
            }
        }

        RenderEverything ();

        if (gameState.mode == WIN)
        {
            RenderMessage ("You Win!");
            gameState.level = NextLevel ();
            gameState.levelNum++;
            if (gameState.level == NULL)
            {
                gameState.level = NextLevel ();
                gameState.baseSpeed++;
                gameState.speedFactor = gameState.baseSpeed;
            }

            ResetBricks (brickPtrs, gameState.level, FALSE);
            ResetBall (ballPtr);
            ResetPaddle (paddlePtr);
            gameState.mode = PREPRESTART;
            gameState.livesLeft++;
            if (gameState.demoMode)
            {
                QuitIf (SDL_Flip (screenPtr) < 0, "Failed to swap buffers.\n");
                SDL_Delay (1000);
                gameState.mode = RUN;
            }
        }
            
        if (gameState.mode == LOSE)
        {
            if (gameState.livesLeft != 0)
            {
                RenderMessage ("Whoopsie.");
                score--; // Adjust penalty for dying here.
                gameState.livesLeft--;
                gameState.mode = PREPRESTART;
                ball.vx = 0;
                ball.vy = 0;
            }
            else
                LoseGame ();
        }

        // Wait until the fps timer expires.
        now = SDL_GetTicks ();
        waitTime = now - startTime;
        if (gameState.mode == RUN)
        {
            maxWait = MAX (maxWait, waitTime);
            minWait = MIN (minWait, waitTime);
        }

        if (waitTime < MS_PER_FRAME && !gameState.fastMode)
            SDL_Delay (MS_PER_FRAME - (now - startTime));

        QuitIf (SDL_Flip (screenPtr) < 0, "Failed to swap buffers.\n");
    }

    if (profiling)
        printf ("Minimum wait time: %u ms.\nMaximum wait time: %u ms.\n",
                minWait, maxWait);
}


static void Usage (void)
{
    printf ("Usage: breakout [options]\n\nOptions:\n"
            "\t-d, --demo:\t\tRun in demo mode.\n"
            "\t-n, --fast:\t\tDon't limit framerate. Push the hardware.\n"
            "\t-s, --speed=num:\tUse num as a speed factor. Default is 2.\n\t"
                              "\t\t\t\tMust be at least 1.\n"
            "\t-w, --windowed:\t\tPlay in windowed mode.\n"
            "\t-f, --fullscreen:\tPlay in fullscreen mode.\n"
            "\t-p, --profiling:\tEnable some timing output.\n"
            "\t-a, --angles:\t\tDon't change the ball angle based on which\n"
                              "\t\t\t\tpart of the paddle it hits.\n"
            "\t-h, -?, --help:\t\tDisplay this information.\n");
}


int main (int argc, char *argv[])
{
    int option, optIdx;
    int speed;

    struct option longOptions[] =
        {
            {"angles",      no_argument, 0, 'a'},
            {"demo",        no_argument, 0, 'd'},
            {"fast",        no_argument, 0, 'n'},
            {"no-limit",    no_argument, 0, 'n'},
            {"windowed",    no_argument, 0, 'w'},
            {"fullscreen",  no_argument, 0, 'f'},
            {"profiling",   no_argument, 0, 'p'},
            {"help",        no_argument, 0, 'h'},
            {"speed", required_argument, 0, 's'},
            {0, 0, 0, 0}
        };

    gameState.demoMode = FALSE;
    gameState.fastMode = FALSE;
    gameState.angles   = TRUE;
    profiling          = FALSE;
    gameState.level = NextLevel ();
    gameState.levelNum = 0;
    optIdx = 0;

    while ((option = getopt_long (argc, argv, "adpfs:wnh?", 
                                  longOptions, &optIdx)) != -1)
    {
        switch (option)
        {
        case 'a':
            gameState.angles = FALSE;
            break;
        case 'd':
            gameState.demoMode = TRUE;
            break;
        case 'n':
            gameState.fastMode = TRUE;
            break;
        case 's':
            speed = strtol (optarg, NULL, 0);
            if (speed < 1)
            {
                fprintf (stderr, "Speed must be greater than 0.\n");
                Usage ();
                return 1;
            }
            if (speed > 100)
                fprintf (stderr, "That's ridiculous. You're not that fast.\n");
            gameState.speedFactor = speed;
            gameState.baseSpeed = speed;
            break;
        case 'w':
            fullScreen = 0;
            break;
        case 'f':
            fullScreen = SDL_FULLSCREEN;
            break;
        case 'p':
            profiling = TRUE;
        case 'h':
        case '?':
            Usage ();
            return 0;
        default:
            Usage ();
            return -1;
        }
    }

    QuitIf (InitHighScores () != SUCCESS,
            "Failed to initialize high scores.\n");
    InitSDL ();
    LoadImages ();
    ResetBricks (brickPtrs, gameState.level, TRUE);
    InitPaddle (paddlePtr);
    InitBall   (ballPtr);
    InitScore  ();

    ClearScreen  ();
    RenderBricks ();
    RenderPaddle ();
    RenderBall   ();
    RenderScore  ();

    QuitIf (SDL_Flip (screenPtr) < 0, "Failed to swap buffers.\n");

    MainLoop ();

    CleanUp ();
    return 0;
}
